/******************************************************************************
 *  Compilation:  javac StdDraw.java
 *  Execution:    java StdDraw
 *  Dependencies: none
 *
 *  Standard drawing library. This class provides a basic capability for
 *  creating drawings with your programs. It uses a simple graphics model that
 *  allows you to create drawings consisting of points, lines, and curves
 *  in a window on your computer and to save the drawings to a file.
 *
 *  Todo
 *  ----
 *    -  Add support for gradient fill, etc.
 *    -  Fix setCanvasSize() so that it can only be called once.
 *    -  On some systems, drawing a line (or other shape) that extends way
 *       beyond canvas (e.g., to infinity) dimensions does not get drawn.
 *
 *  Remarks
 *  -------
 *    -  don't use AffineTransform for rescaling since it inverts
 *       images and strings
 *
 ******************************************************************************/

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.FileDialog;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.RenderingHints;
import java.awt.Toolkit;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

import java.awt.geom.Arc2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;

import java.awt.image.BufferedImage;
import java.awt.image.DirectColorModel;
import java.awt.image.WritableRaster;

import java.io.File;
import java.io.IOException;

import java.net.MalformedURLException;
import java.net.URL;

import java.util.LinkedList;
import java.util.TreeSet;
import java.util.NoSuchElementException;
import javax.imageio.ImageIO;

import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.KeyStroke;

/**
 *  The {@code StdDraw} class provides a basic capability for
 *  creating drawings with your programs. It uses a simple graphics model that
 *  allows you to create drawings consisting of points, lines, squares,
 *  circles, and other geometric shapes in a window on your computer and
 *  to save the drawings to a file. Standard drawing also includes
 *  facilities for text, color, pictures, and animation, along with
 *  user interaction via the keyboard and mouse.
 *  <p>
 *  <b>Getting started.</b>
 *  To use standard drawing, you must have {@code StdDraw.class} in your
 *  Java classpath. If you used our autoinstaller, you should be all set.
 *  Otherwise, download StdDraw.java from
 *  http://introcs.cs.princeton.edu/java/stdlib/StdDraw.java"
 *  and put a copy in your working directory.
 *  <p>
 *  Now, type the following short program into your editor:
 *  <pre>
 *   public class TestStdDraw {
 *       public static void main(String[] args) {
 *           StdDraw.setPenRadius(0.05);
 *           StdDraw.setPenColor(StdDraw.BLUE);
 *           StdDraw.point(0.5, 0.5);
 *           StdDraw.setPenColor(StdDraw.MAGENTA);
 *           StdDraw.line(0.2, 0.2, 0.8, 0.2);
 *       }
 *   }
 *  </pre>
 *  If you compile and execute the program, you should see a window
 *  appear with a thick magenta line and a blue point.
 *  This program illustrates the two main types of methods in standard
 *  drawing—methods that draw geometric shapes and methods that
 *  control drawing parameters.
 *  The methods {@code StdDraw.line()} and {@code StdDraw.point()}
 *  draw lines and points; the methods {@code StdDraw.setPenRadius()}
 *  and {@code StdDraw.setPenColor()} control the line thickness and color.
 *  <p>
 *  <b>Points and lines.</b>
 *  You can draw points and line segments with the following methods:
 *  <ul>
 *  <li> {@link #point(double x, double y)}
 *  <li> {@link #line(double x1, double y1, double x2, double y2)}
 *  </ul>
 *  <p>
 *  The <em>x</em>- and <em>y</em>-coordinates must be in the drawing area
 *  (between 0 and 1 and by default) or the points and lines will
 *  not be visible.
 *  <p>
 *  <b>Squares, circles, rectangles, and ellipses.</b>
 *  You can draw squares, circles, rectangles, and ellipses using
 *  the following methods:
 *  <ul>
 *  <li> {@link #circle(double x, double y, double radius)}
 *  <li> {@link #ellipse(double x, double y, double semiMajorAxis,
          double semiMinorAxis)}
 *  <li> {@link #square(double x, double y, double radius)}
 *  <li> {@link #rectangle(double x, double y, double halfWidth,
          double halfHeight)}
 *  </ul>
 *  <p>
 *  All of these methods take as arguments the location and size of the shape.
 *  The location is always specified by the <em>x</em>-
 *  and <em>y</em>-coordinates of its <em>center</em>.
 *  The size of a circle is specified by its radius and the size of an ellipse
 *  is specified by the lengths of its semi-major and semi-minor axes.
 *  The size of a square or rectangle is specified by its half-width
 *  or half-height.
 *  The convention for drawing squares and rectangles is parallel to those for
 *  drawing circles and ellipses, but may be unexpected to the uninitiated.
 *  <p>
 *  The methods above trace outlines of the given shapes. The following methods
 *  draw filled versions:
 *  <ul>
 *  <li> {@link #filledCircle(double x, double y, double radius)}
 *  <li> {@link #filledEllipse(double x, double y, double semiMajorAxis,
                               double semiMinorAxis)}
 *  <li> {@link #filledSquare(double x, double y, double radius)}
 *  <li> {@link #filledRectangle(double x, double y, double halfWidth,
                                 double halfHeight)}
 *  </ul>
 *  <p>
 *  <b>Circular arcs.</b>
 *  You can draw circular arcs with the following method:
 *  <ul>
 *  <li> {@link #arc(double x, double y, double radius,
                     double angle1, double angle2)}
 *  </ul>
 *  <p>
 *  The arc is from the circle centered at (<em>x</em>, <em>y</em>) of
 *  the specified radius.  The arc extends from angle1 to angle2.
 *  By convention, the angles are
 *  <em>polar</em> (counterclockwise angle from the <em>x</em>-axis)
 *  and represented in degrees.
 *  For example, {@code StdDraw.arc(0.0, 0.0, 1.0, 0, 90)}
 *  draws the arc of the unit circle from 3 o'clock (0 degrees) to
 *  12 o'clock (90 degrees).
 *  <p>
 *  <b>Polygons.</b>
 *  You can draw polygons with the following methods:
 *  <ul>
 *  <li> {@link #polygon(double[] x, double[] y)}
 *  <li> {@link #filledPolygon(double[] x, double[] y)}
 *  </ul>
 *  <p>
 *  The points in the polygon are ({@code x[i]}, {@code y[i]}).
 *  For example, the following code fragment draws a filled diamond
 *  with vertices (0.1, 0.2), (0.2, 0.3), (0.3, 0.2), and (0.2, 0.1):
 *  <pre>
 *   double[] x = { 0.1, 0.2, 0.3, 0.2 };
 *   double[] y = { 0.2, 0.3, 0.2, 0.1 };
 *   StdDraw.filledPolygon(x, y);
 *  </pre>
 *  <p>
 *  <b>Pen size.</b>
 *  The pen is circular, so that when you set the pen radius to <em>r</em>
 *  and draw a point, you get a circle of radius <em>r</em>. Also, lines are
 *  of thickness 2<em>r</em> and have rounded ends. The default pen radius
 *  is 0.005 and is not affected by coordinate scaling. This default pen
 *  radius is about 1/200 the width of the default canvas, so that if
 *  you draw 100 points equally spaced along a horizontal or vertical line,
 *  you will be able to see individual circles, but if you draw 200 such
 *  points, the result will look like a line.
 *  <ul>
 *  <li> {@link #setPenRadius(double radius)}
 *  </ul>
 *  <p>
 *  For example, {@code StdDraw.setPenRadius(0.025)} makes
 *  the thickness of the lines and the size of the points to be five times
 *  the 0.005 default.
 *  To draw points with the minimum possible radius (one pixel on typical
 *  displays), set the pen radius to 0.0.
 *  <p>
 *  <b>Pen color.</b>
 *  All geometric shapes (such as points, lines, and circles) are drawn using
 *  the current pen color. By default, it is black.
 *  You can change the pen color with the following methods:
 *  <ul>
 *  <li> {@link #setPenColor(int red, int green, int blue)}
 *  <li> {@link #setPenColor(Color color)}
 *  </ul>
 *  <p>
 *  The first method allows you to specify colors using the RGB color system.
 *  This <a href = "http://johndyer.name/lab/colorpicker/">color picker</a>
 *  is a convenient way to find a desired color.
 *  The second method allows you to specify colors using the
 *  {@link Color} data type that is discussed in Chapter 3. Until then,
 *  you can use this method with one of these predefined colors
 *  in standard drawing:
 *  {@link #BLACK}, {@link #BLUE}, {@link #CYAN}, {@link #DARK_GRAY},
 *  {@link #GRAY},
 *  {@link #GREEN}, {@link #LIGHT_GRAY}, {@link #MAGENTA}, {@link #ORANGE},
 *  {@link #PINK}, {@link #RED}, {@link #WHITE}, and {@link #YELLOW}.
 *  For example, {@code StdDraw.setPenColor(StdDraw.MAGENTA)} sets the
 *  pen color to magenta.
 *  <p>
 *  <b>Canvas size.</b>
 *  By default, all drawing takes places in a 512-by-512 canvas.
 *  The canvas does not include the window title or window border.
 *  You can change the size of the canvas with the following method:
 *  <ul>
 *  <li> {@link #setCanvasSize(int width, int height)}
 *  </ul>
 *  <p>
 *  This sets the canvas size to be <em>width</em>-by-<em>height</em> pixels.
 *  It also erases the current drawing and resets the coordinate system,
 *  pen radius, pen color, and font back to their default values.
 *  Ordinarly, this method is called once, at the very beginning of a program.
 *  For example, {@code StdDraw.setCanvasSize(800, 800)}
 *  sets the canvas size to be 800-by-800 pixels.
 *  <p>
 *  <b>Canvas scale and coordinate system.</b>
 *  By default, all drawing takes places in the unit square, with (0, 0) at
 *  lower left and (1, 1) at upper right. You can change the default
 *  coordinate system with the following methods:
 *  <ul>
 *  <li> {@link #setXscale(double xmin, double xmax)}
 *  <li> {@link #setYscale(double ymin, double ymax)}
 *  <li> {@link #setScale(double min, double max)}
 *  </ul>
 *  <p>
 *  The arguments are the coordinates of the minimum and maximum
 *  <em>x</em>- or <em>y</em>-coordinates that will appear in the canvas.
 *  For example, if you  wish to use the default coordinate system but
 *  leave a small margin, you can call {@code StdDraw.setScale(-.05, 1.05)}.
 *  <p>
 *  These methods change the coordinate system for subsequent drawing
 *  commands; they do not affect previous drawings.
 *  These methods do not change the canvas size; so, if the <em>x</em>-
 *  and <em>y</em>-scales are different, squares will become rectangles
 *  and circles will become ellipsoidal.
 *  <p>
 *  <b>Text.</b>
 *  You can use the following methods to annotate your drawings with text:
 *  <ul>
 *  <li> {@link #text(double x, double y, String text)}
 *  <li> {@link #text(double x, double y, String text, double degrees)}
 *  <li> {@link #textLeft(double x, double y, String text)}
 *  <li> {@link #textRight(double x, double y, String text)}
 *  </ul>
 *  <p>
 *  The first two methods write the specified text in the current font,
 *  centered at (<em>x</em>, <em>y</em>).
 *  The second method allows you to rotate the text.
 *  The last two methods either left- or right-align the text
 *  at (<em>x</em>, <em>y</em>).
 *  <p>
 *  The default font is a Sans Serif font with point size 16.
 *  You can use the following method to change the font:
 *  <ul>
 *  <li> {@link #setFont(Font font)}
 *  </ul>
 *  <p>
 *  You use the {@link Font} data type to specify the font. This allows you to
 *  choose the face, size, and style of the font. For example, the following
 *  code fragment sets the font to Arial Bold, 60 point.
 *  <pre>
 *   Font font = new Font("Arial", Font.BOLD, 60);
 *   StdDraw.setFont(font);
 *   StdDraw.text(0.5, 0.5, "Hello, World");
 *  </pre>
 *  <p>
 *  <b>Images.</b>
 *  You can use the following methods to add images to your drawings:
 *  <ul>
 *  <li> {@link #picture(double x, double y, String filename)}
 *  <li> {@link #picture(double x, double y, String filename, double degrees)}
 *  <li> {@link #picture(double x, double y, String filename,
                         double scaledWidth, double scaledHeight)}
 *  <li> {@link #picture(double x, double y, String filename,
                         double scaledWidth, double scaledHeight,
                         double degrees)}
 *  </ul>
 *  <p>
 *  These methods draw the specified image, centered
 *  at (<em>x</em>, <em>y</em>).  The supported image formats are JPEG,
 *  PNG, and GIF.  The image will display at its native size, independent
 *  of the coordinate system.  Optionally, you can rotate the image
 *  a specified number of degrees counterclockwise
 *  or rescale it to fit snugly inside a width-by-height bounding box.
 *  <p>
 *  <b>Saving to a file.</b>
 *  You save your image to a file using the <em>File → Save</em> menu option.
 *  You can also save a file programatically using the following method:
 *  <ul>
 *  <li> {@link #save(String filename)}
 *  </ul>
 *  <p>
 *  The supported image formats are JPEG and PNG. The filename must have
 *  either the extension .jpg or .png.
 *  We recommend using PNG for drawing that consist solely of geometric
 *  shapes and JPEG for drawings that contains pictures.
 *  <p>
 *  <b>Clearing the canvas.</b>
 *  To clear the entire drawing canvas, you can use the following methods:
 *  <ul>
 *  <li> {@link #clear()}
 *  <li> {@link #clear(Color color)}
 *  </ul>
 *  <p>
 *  The first method clears the canvas to white; the second method
 *  allows you to specify a color of your choice. For example,
 *  {@code StdDraw.clear(StdDraw.LIGHT_GRAY)} clears the canvas to a shade
 *  of gray.
 *  <p>
 *  <b>Computer animations and double buffering.</b>
 *  Double buffering is one of the most powerful features of standard drawing,
 *  enabling computer animations.
 *  The following methods control the way in which objects are drawn:
 *  <ul>
 *  <li> {@link #enableDoubleBuffering()}
 *  <li> {@link #disableDoubleBuffering()}
 *  <li> {@link #show()}
 *  <li> {@link #pause(int t)}
 *  </ul>
 *  <p>
 *  By default, double buffering is disabled, which means that as soon as you
 *  call a drawing
 *  method—such as {@code point()} or {@code line()}—the
 *  results appear on the screen.
 *  <p>
 *  When double buffering is enabled by calling
 *  {@link #enableDoubleBuffering()}, all drawing takes place on the
 *  <em>offscreen canvas</em>. The offscreen canvas is not displayed.
 *  Only when you call {@link #show()} does your drawing get copied from
 *  the offscreen canvas to the onscreen canvas, where it is displayed
 *  in the standard drawing window. You can think of double buffering
 *  as collecting all of the lines, points, shapes,
 *  and text that you tell it to draw, and then drawing them all
 *  <em>simultaneously</em>, upon request.
 *  <p>
 *  The most important use of double buffering is to produce computer
 *  animations, creating the illusion of motion by rapidly
 *  displaying static drawings. To produce an animation, repeat
 *  the following four steps:
 *  <ul>
 *  <li> Clear the offscreen canvas.
 *  <li> Draw objects on the offscreen canvas.
 *  <li> Copy the offscreen canvas to the onscreen canvas.
 *  <li> Wait for a short while.
 *  </ul>
 *  <p>
 *  The {@link #clear()}, {@link #show()}, and {@link #pause(int dt)} methods
 *  support the first, third, and fourth of these steps, respectively.
 *  <p>
 *  For example, this code fragment animates two balls moving in a circle.
 *  <pre>
 *   StdDraw.setScale(-2, +2);
 *   StdDraw.enableDoubleBuffering();
 *
 *   for (double t = 0.0; true; t += 0.02) {
 *       double x = Math.sin(t);
 *       double y = Math.cos(t);
 *       StdDraw.clear();
 *       StdDraw.filledCircle(x, y, 0.05);
 *       StdDraw.filledCircle(-x, -y, 0.05);
 *       StdDraw.show();
 *       StdDraw.pause(20);
 *   }
 *  </pre>
 *  <p>
 *  <b>Keyboard and mouse inputs.</b>
 *  Standard drawing has very basic support for keyboard and mouse input.
 *  It is much less powerful than most user interface libraries provide,
 *  but also much simpler.  You can use the following methods to intercept
 *  mouse events:
 *  <ul>
 *  <li> {@link #mousePressed()}
 *  <li> {@link #mouseX()}
 *  <li> {@link #mouseY()}
 *  </ul>
 *  <p>
 *  The first method tells you whether a mouse button is currently
 *  being pressed.  The last two methods tells you the <em>x</em>- and
 *  <em>y</em>-coordinates of the mouse's current position, using the
 *  same coordinate system as the canvas (the unit square, by
 *  default).  You should use these methods in an animation loop that
 *  waits a short while before trying to poll the mouse for its
 *  current state.  You can use the following methods to intercept
 *  keyboard events:
\ *  <ul>
 *  <li> {@link #hasNextKeyTyped()}
 *  <li> {@link #nextKeyTyped()}
 *  <li> {@link #isKeyPressed(int keycode)}
 *  </ul>
 *  <p>
 * If the user types lots of keys, they will be saved in a list until
 *  you process them.  The first method tells you whether the user has
 *  typed a key (that your program has not yet processed).  The second
 *  method returns the next key that the user typed (that your program
 *  has not yet processed) and removes it from the list of saved
 *  keystrokes.  The third method tells you whether a key is currently
 *  being pressed.
*  <p>
 *  <b>Accessing control parameters.</b>
 *  You can use the following methods to access the current pen color,
 *  pen radius, and font:
 *  <ul>
 *  <li> {@link #getPenColor()}
 *  <li> {@link #getPenRadius()}
 *  <li> {@link #getFont()}
 *  </ul>
 *  <p>
 *  These methods are useful when you want to temporarily change a
 *  control parameter and reset it back to its original value.
 *  <p>
 *  <b>Corner cases.</b>
 *  To avoid clutter, the API doesn't explicitly refer to arguments that are
 *  null, infinity, or NaN.
 *  <ul>
 *  <li> Any method that is passed a {@code null} argument will throw an
 *       {@link IllegalArgumentException}.
 *  <li> Except as noted in the APIs, drawing an object outside
 *       (or partly outside)
 *       the canvas is permitted. However, only the part of the object that
 *       appears inside the canvas will be visible.
 *  <li> Except as noted in the APIs, all methods accept {@link Double#NaN},
 *       {@link Double#POSITIVE_INFINITY}, and {@link Double#NEGATIVE_INFINITY}
 *       as arugments. An object drawn with an <em>x</em>- or
 *  <em>y</em>-coordinate that is NaN will behave as if it is outside
 *  the canvas, and will not be visible.
 *  </ul>
 *  <p>
 *  <b>Performance tricks.</b>
 *  Standard drawing is capable of drawing large amounts of data.
 *  Here are a few tricks and tips:
 *  <ul>
 *  <li> Use <em>double buffering</em> for static drawing with a large
 *       number of objects.
 *       That is, call {@link #enableDoubleBuffering()} before
 *       the sequence of drawing commands and call {@link #show()} afterwards.
 *       Incrementally displaying a complex drawing while it is being
 *       created can be intolerably inefficient on many computer systems.
 *  <li> When drawing computer animations, call {@code show()}
 *       only once per frame, not after drawing each individual object.
 *  <li> If you call {@code picture()} multiple times with the same filename,
 *       Java will cache the image, so you do not incur the cost of reading
 *       from a file each time.
 *  </ul>
 *  <p>
 *  <b>Known bugs and issues.</b>
 *  <ul>
 *  <li> The {@code picture()} methods may not draw the portion of the
 *       image that is
 *       inside the canvas if the center point (<em>x</em>, <em>y</em>)
 *       is outside the canvas.  This bug appears only on some systems.
 *  <li> Some methods may not draw the portion of the geometric object
 *       that is inside the canvas if the <em>x</em>- or <em>y</em>-coordinates
 *       are infinite.  This bug appears only on some systems.
 *  </ul>
 *  <p>
 *  <b>Reference.</b>
 *  For additional documentation,
 *  see <a href="http://introcs.cs.princeton.edu/15inout">Section 1.5</a> of
 *  <em>Computer Science: An Interdisciplinary Approach</em>
 *  by Robert Sedgewick and Kevin Wayne.
 *
 *  @author Robert Sedgewick
 *  @author Kevin Wayne
 */
public final class StdDraw
    implements ActionListener, MouseListener, MouseMotionListener, KeyListener {

    /**
     *  The color black.
     */
    public static final Color BLACK = Color.BLACK;

    /**
     *  The color blue.
     */
    public static final Color BLUE = Color.BLUE;

    /**
     *  The color cyan.
     */
    public static final Color CYAN = Color.CYAN;

    /**
     *  The color dark gray.
     */
    public static final Color DARK_GRAY = Color.DARK_GRAY;

    /**
     *  The color gray.
     */
    public static final Color GRAY = Color.GRAY;

    /**
     *  The color green.
     */
    public static final Color GREEN  = Color.GREEN;

    /**
     *  The color light gray.
     */
    public static final Color LIGHT_GRAY = Color.LIGHT_GRAY;

    /**
     *  The color magenta.
     */
    public static final Color MAGENTA = Color.MAGENTA;

    /**
     *  The color orange.
     */
    public static final Color ORANGE = Color.ORANGE;

    /**
     *  The color pink.
     */
    public static final Color PINK = Color.PINK;

    /**
     *  The color red.
     */
    public static final Color RED = Color.RED;

    /**
     *  The color white.
     */
    public static final Color WHITE = Color.WHITE;

    /**
     *  The color yellow.
     */
    public static final Color YELLOW = Color.YELLOW;

    /**
     * Shade of blue used in <em>Introduction to Programming in Java</em>.
     * It is Pantone 300U. The RGB values are approximately (9, 90, 166).
     */
    public static final Color BOOK_BLUE = new Color(9, 90, 166);

    /**
     * Shade of light blue used in <em>Introduction to Programming in Java</em>.
     * The RGB values are approximately (103, 198, 243).
     */
    public static final Color BOOK_LIGHT_BLUE = new Color(103, 198, 243);

    /**
     * Shade of red used in <em>Algorithms, 4th edition</em>.
     * It is Pantone 1805U. The RGB values are approximately (150, 35, 31).
     */
    public static final Color BOOK_RED = new Color(150, 35, 31);

    /** Default colors. */
    private static final Color
        DEFAULT_PEN_COLOR   = BLACK,
        DEFAULT_CLEAR_COLOR = WHITE;

    /** Current pen color. */
    private static Color penColor;

    /** Default canvas size is DEFAULT_SIZE-by-DEFAULT_SIZE. */
    private static final int
        DEFAULT_SIZE = 512;
    /** Current canvas size. */
    private static int
        width  = DEFAULT_SIZE,
        height = DEFAULT_SIZE;

    /** Default pen radius. */
    private static final double DEFAULT_PEN_RADIUS = 0.002;

    /** Current pen radius. */
    private static double penRadius;

    /** False iff we draw immediately on show; if true, wait until next show. */
    private static boolean defer = false;

    /* private static final double BORDER = 0.05; */

    /** Default boundary and dimensions of drawing canvas, 0% border. */
    private static final double
        BORDER = 0.00,
        DEFAULT_XMIN = 0.0,
        DEFAULT_XMAX = 1.0,
        DEFAULT_YMIN = 0.0,
        DEFAULT_YMAX = 1.0;
    /** Current boundaries of canvas. */
    private static double xmin, ymin, xmax, ymax;

    /** Synchronizing objects. */
    private static Object
        mouseLock = new Object(),
        keyLock = new Object();

    /** Default font. */
    private static final Font DEFAULT_FONT =
        new Font("SansSerif", Font.PLAIN, 16);

    /** Current font. */
    private static Font font;

    /** Double-buffering images. */
    private static BufferedImage offscreenImage, onscreenImage;
    /** Double-buffering graphics. */
    private static Graphics2D offscreen, onscreen;

    /** Singleton for callbacks: avoids generation of extra .class files. */
    private static StdDraw std = new StdDraw();

    /** The frame for drawing to the screen. */
    private static JFrame frame;

    /* Mouse state. */
    /** True when mouse is currently pressed. */
    private static boolean mousePressed = false;
    /** Current mouse position. */
    private static double
        mouseX = 0,
        mouseY = 0;

    /** Queue of typed key characters. */
    private static LinkedList<Character> keysTyped
        = new LinkedList<Character>();

    /** Set of key codes currently pressed down. */
    private static TreeSet<Integer> keysDown = new TreeSet<Integer>();

    /** Time in milliseconds (from currentTimeMillis()) when we can draw again.
     *  Used to control the frame rate. */
    private static long nextDraw = -1;

    /** Singleton pattern: client can't instantiate. */
    private StdDraw() { }

    static {
        init();
    }

    /**
     * Sets the canvas (drawing area) to be 512-by-512 pixels.
     * This also erases the current drawing and resets the coordinate system,
     * pen radius, pen color, and font back to their default values.
     * Ordinarly, this method is called once, at the very beginning
     * of a program.
     */
    public static void setCanvasSize() {
        setCanvasSize(DEFAULT_SIZE, DEFAULT_SIZE);
    }

    /**
     * Sets the canvas (drawing area) to be CANVASWIDTH>0 by
     * CANVASHEIGHT>0 pixels.  This also erases the current drawing
     * and resets the coordinate system, pen radius, pen color, and
     * font back to their default values.  Ordinarly, this method is
     * called once, at the very beginning of a program.
     */
    public static void setCanvasSize(int canvasWidth, int canvasHeight) {
        if (canvasWidth <= 0 || canvasHeight <= 0) {
            throw err("width and height must be positive");
        }
        width = canvasWidth;
        height = canvasHeight;
        init();
    }

    /** Initialize static variables. */
    private static void init() {
        if (frame != null) {
            frame.setVisible(false);
        }
        frame = new JFrame();
        offscreenImage =
            new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        onscreenImage =
            new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        offscreen = offscreenImage.createGraphics();
        onscreen  = onscreenImage.createGraphics();
        setXscale();
        setYscale();
        offscreen.setColor(DEFAULT_CLEAR_COLOR);
        offscreen.fillRect(0, 0, width, height);
        setPenColor();
        setPenRadius();
        setFont();
        clear();

        RenderingHints hints =
            new RenderingHints(RenderingHints.KEY_ANTIALIASING,
                               RenderingHints.VALUE_ANTIALIAS_ON);
        hints.put(RenderingHints.KEY_RENDERING,
                  RenderingHints.VALUE_RENDER_QUALITY);
        offscreen.addRenderingHints(hints);

        ImageIcon icon = new ImageIcon(onscreenImage);
        JLabel draw = new JLabel(icon);

        draw.addMouseListener(std);
        draw.addMouseMotionListener(std);

        frame.setContentPane(draw);
        frame.addKeyListener(std);
        frame.setResizable(false);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setTitle("Standard Draw");
        frame.setJMenuBar(createMenuBar());
        frame.pack();
        frame.requestFocusInWindow();
        frame.setVisible(true);
    }

    /** Create and return the menu bar (changed to private). */
    private static JMenuBar createMenuBar() {
        JMenuBar menuBar = new JMenuBar();
        JMenu menu = new JMenu("File");
        menuBar.add(menu);
        JMenuItem menuItem1 = new JMenuItem(" Save...   ");
        menuItem1.addActionListener(std);
        int keyMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
        menuItem1.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S,
                                                        keyMask));
        menu.add(menuItem1);
        return menuBar;
    }


   /***************************************************************************
    *  User and screen coordinate systems.
    ***************************************************************************/

    /**
     * Sets the <em>x</em>-scale to be the default (between 0.0 and 1.0).
     */
    public static void setXscale() {
        setXscale(DEFAULT_XMIN, DEFAULT_XMAX);
    }

    /**
     * Sets the <em>y</em>-scale to be the default (between 0.0 and 1.0).
     */
    public static void setYscale() {
        setYscale(DEFAULT_YMIN, DEFAULT_YMAX);
    }

    /**
     * Sets the <em>x</em>-scale and <em>y</em>-scale to be the default
     * (between 0.0 and 1.0).
     */
    public static void setScale() {
        setXscale();
        setYscale();
    }

    /**
     * Sets the <em>x</em>-scale to the specified range.
     *
     * @param  min the minimum value of the <em>x</em>-scale
     * @param  max the maximum value of the <em>x</em>-scale
     * @throws IllegalArgumentException if {@code (max == min)}
     */
    public static void setXscale(double min, double max) {
        double size = max - min;
        if (size == 0.0) {
            throw err("the min and max are the same");
        }
        synchronized (mouseLock) {
            xmin = min - BORDER * size;
            xmax = max + BORDER * size;
        }
    }

    /**
     * Sets the <em>y</em>-scale to the specified range.
     *
     * @param  min the minimum value of the <em>y</em>-scale
     * @param  max the maximum value of the <em>y</em>-scale
     * @throws IllegalArgumentException if {@code (max == min)}
     */
    public static void setYscale(double min, double max) {
        double size = max - min;
        if (size == 0.0) {
            throw err("the min and max are the same");
        }
        synchronized (mouseLock) {
            ymin = min - BORDER * size;
            ymax = max + BORDER * size;
        }
    }

    /**
     * Sets both the <em>x</em>-scale and <em>y</em>-scale to
     * (MIN, MAX).
     */
    public static void setScale(double min, double max) {
        double size = max - min;
        if (size == 0.0) {
            throw err("the min and max are the same");
        }
        synchronized (mouseLock) {
            xmin = min - BORDER * size;
            xmax = max + BORDER * size;
            ymin = min - BORDER * size;
            ymax = max + BORDER * size;
        }
    }

    /* Helper functions that scale from user coordinates to
     * screen coordinates and back. */

    /** Return X in screen coordinates. */
    private static double  scaleX(double x) {
        return width  * (x - xmin) / (xmax - xmin);
    }
    /** Return Y in screen coordinates. */
    private static double  scaleY(double y) {
        return height * (ymax - y) / (ymax - ymin);
    }
    /** Return W scaled to horizontal screen coordinates. */
    private static double factorX(double w) {
        return w * width  / Math.abs(xmax - xmin);
    }
    /** Return H scaled to vertical screen coordinates. */
    private static double factorY(double h) {
        return h * height / Math.abs(ymax - ymin);
    }
    /** Return X in user coordinates. */
    private static double   userX(double x) {
        return xmin + x * (xmax - xmin) / width;
    }
    /** Return Y in user coordinates. */
    private static double   userY(double y) {
        return ymax - y * (ymax - ymin) / height;
    }

    /**
     * Clears the screen to the default color (white).
     */
    public static void clear() {
        clear(DEFAULT_CLEAR_COLOR);
    }

    /**
     * Clears the screen to the specified color.
     *
     * @param color the color to make the background
     */
    public static void clear(Color color) {
        offscreen.setColor(color);
        offscreen.fillRect(0, 0, width, height);
        offscreen.setColor(penColor);
        draw();
    }

    /**
     * Returns the current pen radius.
     *
     * @return the current value of the pen radius
     */
    public static double getPenRadius() {
        return penRadius;
    }

    /**
     * Sets the pen size to the default size (0.002).
     * The pen is circular, so that lines have rounded ends, and when you
     * set the pen radius and draw a point, you get a circle of the
     * specified radius. The pen radius is not affected by coordinate scaling.
     */
    public static void setPenRadius() {
        setPenRadius(DEFAULT_PEN_RADIUS);
    }

    /**
     * Sets the radius of the pen to the specified size.
     * The pen is circular, so that lines have rounded ends, and when
     * you set the pen radius and draw a point, you get a circle of the
     * specified radius.
     * The pen radius is not affected by coordinate scaling.
     *
     * @param  radius the radius of the pen
     * @throws IllegalArgumentException if {@code radius} is negative
     */
    public static void setPenRadius(double radius) {
        if (radius < 0) {
            throw err("pen radius must be nonnegative");
        }
        penRadius = radius;
        float scaledPenRadius = (float) (radius * DEFAULT_SIZE);
        BasicStroke stroke = new BasicStroke(scaledPenRadius,
                                             BasicStroke.CAP_ROUND,
                                             BasicStroke.JOIN_ROUND);
        offscreen.setStroke(stroke);
    }

    /**
     * Returns the current pen color.
     *
     * @return the current pen color
     */
    public static Color getPenColor() {
        return penColor;
    }

    /**
     * Set the pen color to the default color (black).
     */
    public static void setPenColor() {
        setPenColor(DEFAULT_PEN_COLOR);
    }

    /**
     * Sets the pen color to the specified color.
     * <p>
     * The predefined pen colors are
     * {@code StdDraw.BLACK}, {@code StdDraw.BLUE}, {@code StdDraw.CYAN},
     * {@code StdDraw.DARK_GRAY}, {@code StdDraw.GRAY}, {@code StdDraw.GREEN},
     * {@code StdDraw.LIGHT_GRAY}, {@code StdDraw.MAGENTA},
     * {@code StdDraw.ORANGE},
     * {@code StdDraw.PINK}, {@code StdDraw.RED}, {@code StdDraw.WHITE}, and
     * {@code StdDraw.YELLOW}.
     *
     * @param color the color to make the pen
     */
    public static void setPenColor(Color color) {
        if (color == null) {
            throw err();
        }
        penColor = color;
        offscreen.setColor(penColor);
    }

    /**
     * Sets the pen color to the specified RGB color.
     *
     * @param  red the amount of red (between 0 and 255)
     * @param  green the amount of green (between 0 and 255)
     * @param  blue the amount of blue (between 0 and 255)
     * @throws IllegalArgumentException if {@code red}, {@code green},
     *         or {@code blue} is outside its prescribed range
     */
    public static void setPenColor(int red, int green, int blue) {
        if (red   < 0 || red   >= 256) {
            throw err("amount of red must be between 0 and 255");
        }
        if (green < 0 || green >= 256) {
            throw err("amount of green must be between 0 and 255");
        }
        if (blue  < 0 || blue  >= 256) {
            throw err("amount of blue must be between 0 and 255");
        }
        setPenColor(new Color(red, green, blue));
    }

    /**
     * Returns the current font.
     *
     * @return the current font
     */
    public static Font getFont() {
        return font;
    }

    /**
     * Sets the font to the default font (sans serif, 16 point).
     */
    public static void setFont() {
        setFont(DEFAULT_FONT);
    }

    /**
     * Sets the font to FNT.
     */
    public static void setFont(Font fnt) {
        if (fnt == null) {
            throw err();
        }
        StdDraw.font = fnt;
    }


   /************************************************************************
    *  Drawing geometric shapes.
    ************************************************************************/

    /**
     * Draws a line segment between (X0, Y0) and (X1, Y1).
     */
    public static void line(double x0, double y0, double x1, double y1) {
        offscreen.draw(new Line2D.Double(scaleX(x0),
                                         scaleY(y0), scaleX(x1),
                                         scaleY(y1)));
        draw();
    }

    /**
     * Draws one pixel at (X, Y).
     * This method is private because pixels depend on the display.
     * To achieve the same effect, set the pen radius to 0 and
     * call {@code point()}.
     */
    private static void pixel(double x, double y) {
        offscreen.fillRect((int) Math.round(scaleX(x)),
                           (int) Math.round(scaleY(y)), 1, 1);
    }

    /**
     * Draws a point centered at (X, Y)./
     * The point is a filled circle whose radius is equal to the pen radius.
     * To draw a single-pixel point, first set the pen radius to 0.
     */
    public static void point(double x, double y) {
        double xs = scaleX(x);
        double ys = scaleY(y);
        double r = penRadius;
        float scaledPenRadius = (float) (r * DEFAULT_SIZE);

        if (scaledPenRadius <= 1) {
            pixel(x, y);
        } else {
            offscreen.fill(new Ellipse2D.Double(xs - scaledPenRadius / 2,
                                                ys - scaledPenRadius / 2,
                                                scaledPenRadius,
                                                scaledPenRadius));
        }
        draw();
    }

    /**
     * Draws a circle of the specified RADIUS, centered at (X, Y).
     */
    public static void circle(double x, double y, double radius) {
        if (radius < 0) {
            throw err("radius must be nonnegative");
        }
        double xs = scaleX(x);
        double ys = scaleY(y);
        double ws = factorX(2 * radius);
        double hs = factorY(2 * radius);
        if (ws <= 1 && hs <= 1) {
            pixel(x, y);
        } else {
            offscreen.draw(new Ellipse2D.Double(xs - ws / 2, ys - hs / 2,
                                                ws, hs));
        }
        draw();
    }

    /**
     * Draws a filled circle of the specified RADIUS, centered at (X, Y).
     */
    public static void filledCircle(double x, double y, double radius) {
        if (radius < 0) {
            throw err("radius must be nonnegative");
        }
        double xs = scaleX(x);
        double ys = scaleY(y);
        double ws = factorX(2 * radius);
        double hs = factorY(2 * radius);
        if (ws <= 1 && hs <= 1) {
            pixel(x, y);
        } else {
            offscreen.fill(new Ellipse2D.Double(xs - ws / 2, ys - hs / 2,
                                                ws, hs));
        }
        draw();
    }


    /**
     * Draws an ellipse with axes SEMIMAJORAXIS and SEMIMINORAXIS,
     * centered at (X, Y).
     */
    public static void ellipse(double x, double y,
                               double semiMajorAxis, double semiMinorAxis) {
        if (semiMajorAxis < 0) {
            throw err("ellipse semimajor axis must be nonnegative");
        }
        if (semiMinorAxis < 0) {
            throw err("ellipse semiminor axis must be nonnegative");
        }
        double xs = scaleX(x);
        double ys = scaleY(y);
        double ws = factorX(2 * semiMajorAxis);
        double hs = factorY(2 * semiMinorAxis);
        if (ws <= 1 && hs <= 1) {
            pixel(x, y);
        } else {
            offscreen.draw(new Ellipse2D.Double(xs - ws / 2, ys - hs / 2,
                                                ws, hs));
        }
        draw();
    }

    /**
     * Draws a filled ellipse with axes SEMIMAJORAXIS and SEMIMINORAXIS,
     * centered at (X, Y).
     */
    public static void filledEllipse(double x, double y,
                                     double semiMajorAxis,
                                     double semiMinorAxis) {
        if (semiMajorAxis < 0) {
            throw err("ellipse semimajor axis must be nonnegative");
        }
        if (semiMinorAxis < 0) {
            throw err("ellipse semiminor axis must be nonnegative");
        }
        double xs = scaleX(x);
        double ys = scaleY(y);
        double ws = factorX(2 * semiMajorAxis);
        double hs = factorY(2 * semiMinorAxis);
        if (ws <= 1 && hs <= 1) {
            pixel(x, y);
        } else {
            offscreen.fill(new Ellipse2D.Double(xs - ws / 2, ys - hs / 2,
                                                ws, hs));
        }
        draw();
    }


    /**
     * Draws a circular arc of the specified RADIUS,
     * centered at (X, Y), from ANGLE1 to ANGLE2 (in degrees).
     */
    public static void arc(double x, double y, double radius,
                           double angle1, double angle2) {
        if (radius < 0) {
            throw err("arc radius must be nonnegative");
        }
        while (angle2 < angle1) {
            angle2 += 360;
        }
        double xs = scaleX(x);
        double ys = scaleY(y);
        double ws = factorX(2 * radius);
        double hs = factorY(2 * radius);
        if (ws <= 1 && hs <= 1) {
            pixel(x, y);
        } else {
            offscreen.draw(new Arc2D.Double(xs - ws / 2, ys - hs / 2, ws, hs,
                                            angle1, angle2 - angle1,
                                            Arc2D.OPEN));
        }
        draw();
    }

    /**
     * Draws a square of side length 2r, centered at (<em>x</em>, <em>y</em>).
     *
     * @param  x the <em>x</em>-coordinate of the center of the square
     * @param  y the <em>y</em>-coordinate of the center of the square
     * @param  halfLength one half the length of any side of the square
     * @throws IllegalArgumentException if {@code halfLength} is negative
     */
    public static void square(double x, double y, double halfLength) {
        if (halfLength < 0) {
            throw err("half length must be nonnegative");
        }
        double xs = scaleX(x);
        double ys = scaleY(y);
        double ws = factorX(2 * halfLength);
        double hs = factorY(2 * halfLength);
        if (ws <= 1 && hs <= 1) {
            pixel(x, y);
        } else {
            offscreen.draw(new Rectangle2D.Double(xs - ws / 2, ys - hs / 2,
                                                  ws, hs));
        }
        draw();
    }

    /**
     * Draws a filled square of with side 2 * HALFLENGTH, centered at (X, Y).
     */
    public static void filledSquare(double x, double y, double halfLength) {
        if (halfLength < 0) {
            throw err("half length must be nonnegative");
        }
        double xs = scaleX(x);
        double ys = scaleY(y);
        double ws = factorX(2 * halfLength);
        double hs = factorY(2 * halfLength);
        if (ws <= 1 && hs <= 1) {
            pixel(x, y);
        } else {
            offscreen.fill(new Rectangle2D.Double(xs - ws / 2, ys - hs / 2,
                                                  ws, hs));
        }
        draw();
    }


    /**
     * Draws a rectangle of size 2 * HALFWIDTH by 2 * HALFHEIGHT,
     * centered at (X, Y).
     */
    public static void rectangle(double x, double y,
                                 double halfWidth, double halfHeight) {
        if (halfWidth  < 0) {
            throw err("half width must be nonnegative");
        }
        if (halfHeight < 0) {
            throw err("half height must be nonnegative");
        }
        double xs = scaleX(x);
        double ys = scaleY(y);
        double ws = factorX(2 * halfWidth);
        double hs = factorY(2 * halfHeight);
        if (ws <= 1 && hs <= 1) {
            pixel(x, y);
        } else {
            offscreen.draw(new Rectangle2D.Double(xs - ws / 2, ys - hs / 2,
                                                  ws, hs));
        }
        draw();
    }

    /**
     * Draws a filled rectangle of size 2 * HALFWIDTH by 2 * HALFHEIGHT,
     * centered at (X, Y).
     */
    public static void filledRectangle(double x, double y,
                                       double halfWidth, double halfHeight) {
        if (halfWidth < 0) {
            throw err("half width must be nonnegative");
        }
        if (halfHeight < 0) {
            throw err("half height must be nonnegative");
        }
        double xs = scaleX(x);
        double ys = scaleY(y);
        double ws = factorX(2 * halfWidth);
        double hs = factorY(2 * halfHeight);
        if (ws <= 1 && hs <= 1) {
            pixel(x, y);
        } else {
            offscreen.fill(new Rectangle2D.Double(xs - ws / 2, ys - hs / 2,
                                                  ws, hs));
        }
        draw();
    }


    /**
     * Draws a polygon with the vertices (X[0], Y[0]), (X[1], Y[1]), ....
     */
    public static void polygon(double[] x, double[] y) {
        if (x == null) {
            throw err();
        }
        if (y == null) {
            throw err();
        }
        int n1 = x.length;
        int n2 = y.length;
        if (n1 != n2) {
            throw err("arrays must be of the same length");
        }
        int n = n1;
        GeneralPath path = new GeneralPath();
        path.moveTo((float) scaleX(x[0]), (float) scaleY(y[0]));
        for (int i = 0; i < n; i++) {
            path.lineTo((float) scaleX(x[i]), (float) scaleY(y[i]));
        }
        path.closePath();
        offscreen.draw(path);
        draw();
    }

    /**
     * Draws a filled polygon with the vertices (X[0], Y[0]), (X[1], Y[1]),
     * ....
     */
    public static void filledPolygon(double[] x, double[] y) {
        if (x == null) {
            throw err();
        }
        if (y == null) {
            throw err();
        }
        int n1 = x.length;
        int n2 = y.length;
        if (n1 != n2) {
            throw err("arrays must be of the same length");
        }
        int n = n1;
        GeneralPath path = new GeneralPath();
        path.moveTo((float) scaleX(x[0]), (float) scaleY(y[0]));
        for (int i = 0; i < n; i++) {
            path.lineTo((float) scaleX(x[i]), (float) scaleY(y[i]));
        }
        path.closePath();
        offscreen.fill(path);
        draw();
    }


   /***************************************************************************
    *  Drawing images.
    ***************************************************************************/
    /** Return an image read from FILENAME. */
    private static Image getImage(String filename) {
        if (filename == null) {
            throw err();
        }

        ImageIcon icon = new ImageIcon(filename);

        if (icon == null
            || icon.getImageLoadStatus() != MediaTracker.COMPLETE) {
            try {
                URL url = new URL(filename);
                icon = new ImageIcon(url);
            } catch (MalformedURLException e) {
                /* Ignore MalformedException */
            }
        }

        if (icon == null
            || icon.getImageLoadStatus() != MediaTracker.COMPLETE) {
            URL url = StdDraw.class.getResource(filename);
            if (url != null) {
                icon = new ImageIcon(url);
            }
        }

        if (icon == null
            || icon.getImageLoadStatus() != MediaTracker.COMPLETE) {
            URL url = StdDraw.class.getResource("/" + filename);
            if (url == null) {
                throw err("image " + filename + " not found");
            }
            icon = new ImageIcon(url);
        }

        return icon.getImage();
    }

   /***************************************************************************
    * [Summer 2016] Should we update to use ImageIO instead of ImageIcon()?
    *               Seems to have some issues loading images on some systems
    *               and slows things down on other systems.
    *               especially if you don't call ImageIO.setUseCache(false)
    *               One advantage is that it returns a BufferedImage.
    ***************************************************************************/
/*
    private static BufferedImage getImage(String filename) {
        if (filename == null) throw err();

        // from a file or URL
        try {
            URL url = new URL(filename);
            BufferedImage image = ImageIO.read(url);
            return image;
        }
        catch (IOException e) {
            // ignore
        }

        // in case file is inside a .jar (classpath relative to StdDraw)
        try {
            URL url = StdDraw.class.getResource(filename);
            BufferedImage image = ImageIO.read(url);
            return image;
        }
        catch (IOException e) {
            // ignore
        }

        // in case file is inside a .jar (classpath relative to root of jar)
        try {
            URL url = StdDraw.class.getResource("/" + filename);
            BufferedImage image = ImageIO.read(url);
            return image;
        }
        catch (IOException e) {
            // ignore
        }
        throw err("image " + filename + " not found");
    }
*/
    /**
     * Draws the image contained in FILENAME centered at (X, Y).
     * The supported image formats are JPEG, PNG, and GIF.
     * As an optimization, the picture is cached, so there is no performance
     * penalty for redrawing the same image multiple times (e.g.,
     * in an animation).  However, if you change the picture file after
     * drawing it, subsequent calls will draw the original picture.
     */
    public static void picture(double x, double y, String filename) {
        Image image = getImage(filename);
        double xs = scaleX(x);
        double ys = scaleY(y);
        int ws = image.getWidth(null);
        int hs = image.getHeight(null);
        if (ws < 0 || hs < 0) {
            throw err("image " + filename + " is corrupt");
        }

        offscreen.drawImage(image, (int) Math.round(xs - ws / 2.0),
                            (int) Math.round(ys - hs / 2.0), null);
        draw();
    }

    /**
     * Draws the image contained in FILENAME centered at (X, Y),
     * rotated by DEGREES. The supported image formats are JPEG, PNG, and GIF.
     */
    public static void picture(double x, double y,
                               String filename, double degrees) {
        Image image = getImage(filename);
        double xs = scaleX(x);
        double ys = scaleY(y);
        int ws = image.getWidth(null);
        int hs = image.getHeight(null);
        if (ws < 0 || hs < 0) {
            throw err("image " + filename + " is corrupt");
        }

        offscreen.rotate(Math.toRadians(-degrees), xs, ys);
        offscreen.drawImage(image, (int) Math.round(xs - ws / 2.0),
                            (int) Math.round(ys - hs / 2.0), null);
        offscreen.rotate(Math.toRadians(+degrees), xs, ys);

        draw();
    }

    /**
     * Draws the image contained in FILENAME centered at (X, Y) and
     * rescaled to SCALEDWIDTH by SCALEDHEIGHT.
     * The supported image formats are JPEG, PNG, and GIF.
     */
    public static void picture(double x, double y, String filename,
                               double scaledWidth, double scaledHeight) {
        Image image = getImage(filename);
        if (scaledWidth  < 0) {
            throw err("width  is negative: " + scaledWidth);
        }
        if (scaledHeight < 0) {
            throw err("height is negative: " + scaledHeight);
        }
        double xs = scaleX(x);
        double ys = scaleY(y);
        double ws = factorX(scaledWidth);
        double hs = factorY(scaledHeight);
        if (ws < 0 || hs < 0) {
            throw err("image " + filename + " is corrupt");
        }
        if (ws <= 1 && hs <= 1) {
            pixel(x, y);
        } else {
            offscreen.drawImage(image, (int) Math.round(xs - ws / 2.0),
                                       (int) Math.round(ys - hs / 2.0),
                                       (int) Math.round(ws),
                                       (int) Math.round(hs), null);
        }
        draw();
    }


    /**
     * Draws the image contained in FILENAME centered at (X, Y), rotated by
     * DEGREES, and rescaled to SCALEDWIDTH by SCALEDHEIGHT.
     * The supported image formats are JPEG, PNG, and GIF.
     */
    public static void picture(double x, double y, String filename,
                               double scaledWidth, double scaledHeight,
                               double degrees) {
        if (scaledWidth < 0) {
            throw err("width is negative: " + scaledWidth);
        }
        if (scaledHeight < 0) {
            throw err("height is negative: " + scaledHeight);
        }
        Image image = getImage(filename);
        double xs = scaleX(x);
        double ys = scaleY(y);
        double ws = factorX(scaledWidth);
        double hs = factorY(scaledHeight);
        if (ws < 0 || hs < 0) {
            throw err("image " + filename + " is corrupt");
        }
        if (ws <= 1 && hs <= 1) {
            pixel(x, y);
        }

        offscreen.rotate(Math.toRadians(-degrees), xs, ys);
        offscreen.drawImage(image, (int) Math.round(xs - ws / 2.0),
                                   (int) Math.round(ys - hs / 2.0),
                                   (int) Math.round(ws),
                                   (int) Math.round(hs), null);
        offscreen.rotate(Math.toRadians(+degrees), xs, ys);

        draw();
    }

   /***************************************************************************
    *  Drawing text.
    ***************************************************************************/

    /**
     * Write TEXT in the current font, centered at (X, Y).
     */
    public static void text(double x, double y, String text) {
        if (text == null) {
            throw err();
        }
        offscreen.setFont(font);
        FontMetrics metrics = offscreen.getFontMetrics();
        double xs = scaleX(x);
        double ys = scaleY(y);
        int ws = metrics.stringWidth(text);
        int hs = metrics.getDescent();
        offscreen.drawString(text, (float) (xs - ws / 2.0), (float) (ys + hs));
        draw();
    }

    /**
     * Write TEXT in the current font, centered at (X, Y), and
     * rotated DEGREES.
     */
    public static void text(double x, double y, String text, double degrees) {
        if (text == null) {
            throw err();
        }
        double xs = scaleX(x);
        double ys = scaleY(y);
        offscreen.rotate(Math.toRadians(-degrees), xs, ys);
        text(x, y, text);
        offscreen.rotate(Math.toRadians(+degrees), xs, ys);
    }


    /** Write TEXT in the current font, left-aligned at (X, Y). */
    public static void textLeft(double x, double y, String text) {
        if (text == null) {
            throw err();
        }
        offscreen.setFont(font);
        FontMetrics metrics = offscreen.getFontMetrics();
        double xs = scaleX(x);
        double ys = scaleY(y);
        int hs = metrics.getDescent();
        offscreen.drawString(text, (float) xs, (float) (ys + hs));
        draw();
    }

    /** Write TEXT in the current font, right-aligned at (X, Y). */
    public static void textRight(double x, double y, String text) {
        if (text == null) {
            throw err();
        }
        offscreen.setFont(font);
        FontMetrics metrics = offscreen.getFontMetrics();
        double xs = scaleX(x);
        double ys = scaleY(y);
        int ws = metrics.stringWidth(text);
        int hs = metrics.getDescent();
        offscreen.drawString(text, (float) (xs - ws), (float) (ys + hs));
        draw();
    }


    /**
     * Copies the offscreen buffer to the onscreen buffer, pauses for T
     * milliseconds and enables double buffering.
     * @deprecated replaced by
     * {@link #enableDoubleBuffering}, {@link #show()}, and {@link #pause}
     */
    @Deprecated
    public static void show(int t) {
        long millis = System.currentTimeMillis();
        if (millis < nextDraw) {
            try {
                Thread.sleep(nextDraw - millis);
            } catch (InterruptedException e) {
                System.out.println("Error sleeping");
            }
            millis = nextDraw;
        }

        show();
        enableDoubleBuffering();

        nextDraw = millis + t;
    }

    /**
     * Pause for T milliseconds. This method is intended to support computer
     * animations.
     */
    public static void pause(int t) {
        long millis = System.currentTimeMillis();
        if (millis < nextDraw) {
            try {
                Thread.sleep(nextDraw - millis);
            } catch (InterruptedException e) {
                System.out.println("Error sleeping");
            }
            millis = nextDraw;
        }

        nextDraw = millis + t;
    }

    /**
     * Copies offscreen buffer to onscreen buffer. There is no reason to call
     * this method unless double buffering is enabled.
     */
    public static void show() {
        onscreen.drawImage(offscreenImage, 0, 0, null);
        frame.repaint();
    }

    /** Draw onscreen if defer is false. */
    private static void draw() {
        if (!defer) {
            show();
        }
    }

    /**
     * Enable double buffering. All subsequent calls to
     * drawing methods such as {@code line()}, {@code circle()},
     * and {@code square()} will be deferred until the next call
     * to show(). Useful for animations.
     */
    public static void enableDoubleBuffering() {
        defer = true;
    }

    /**
     * Disable double buffering. All subsequent calls to
     * drawing methods such as {@code line()}, {@code circle()},
     * and {@code square()} will be displayed on screen when called.
     * This is the default.
     */
    public static void disableDoubleBuffering() {
        defer = false;
    }


   /***************************************************************************
    *  Save drawing to a file.
    ***************************************************************************/

    /**
     * Saves the drawing to FILENAME.
     * The supported image formats are JPEG and PNG;
     * the filename suffix must be {@code .jpg} or {@code .png}.
     */
    public static void save(String filename) {
        /* Need to change from ARGB to RGB for JPEG
         * See: http://archives.java.sun.com/cgi-bin
         *       /wa?A2=ind0404&L=java2d-interest&D=0&P=2727
         */

        if (filename == null) {
            throw err();
        }
        File file = new File(filename);
        String suffix = filename.substring(filename.lastIndexOf('.') + 1);

        if ("png".equalsIgnoreCase(suffix)) {
            try {
                ImageIO.write(onscreenImage, suffix, file);
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else if ("jpg".equalsIgnoreCase(suffix)) {
            WritableRaster raster = onscreenImage.getRaster();
            WritableRaster newRaster;
            newRaster = raster.createWritableChild(0, 0, width, height,
                                                   0, 0, new int[] {0, 1, 2});
            DirectColorModel cm =
                (DirectColorModel) onscreenImage.getColorModel();
            DirectColorModel newCM = new DirectColorModel(cm.getPixelSize(),
                                                          cm.getRedMask(),
                                                          cm.getGreenMask(),
                                                          cm.getBlueMask());
            BufferedImage rgbBuffer =
                new BufferedImage(newCM, newRaster, false,  null);
            try {
                ImageIO.write(rgbBuffer, suffix, file);
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println("Invalid image file type: " + suffix);
        }
    }


    /* This method must not be called directly. */
    @Override
    public void actionPerformed(ActionEvent e) {
        FileDialog chooser = new FileDialog(StdDraw.frame,
                                            "Use a .png or .jpg extension",
                                            FileDialog.SAVE);
        chooser.setVisible(true);
        String filename = chooser.getFile();
        if (filename != null) {
            StdDraw.save(chooser.getDirectory() + File.separator
                         + chooser.getFile());
        }
    }


   /***************************************************************************
    *  Mouse interactions.
    ***************************************************************************/

    /**
     * Returns true iff the mouse is being pressed.
     */
    public static boolean mousePressed() {
        synchronized (mouseLock) {
            return mousePressed;
        }
    }

    /**
     * Returns the x-coordinate of the mouse.
     */
    public static double mouseX() {
        synchronized (mouseLock) {
            return mouseX;
        }
    }

    /**
     * Returns the y-coordinate of the mouse.
     */
    public static double mouseY() {
        synchronized (mouseLock) {
            return mouseY;
        }
    }


    /* This method must not be called directly. */
    @Override
    public void mouseClicked(MouseEvent e) {
    }

    /* This method must not be called directly. */
    @Override
    public void mouseEntered(MouseEvent e) {
    }

    /* This method must not be called directly. */
    @Override
    public void mouseExited(MouseEvent e) {
    }

    /* This method must not be called directly. */
    @Override
    public void mousePressed(MouseEvent e) {
        synchronized (mouseLock) {
            mouseX = StdDraw.userX(e.getX());
            mouseY = StdDraw.userY(e.getY());
            mousePressed = true;
        }
    }

    /* This method must not be called directly. */
    @Override
    public void mouseReleased(MouseEvent e) {
        synchronized (mouseLock) {
            mousePressed = false;
        }
    }

    /* This method must not be called directly. */
    @Override
    public void mouseDragged(MouseEvent e)  {
        synchronized (mouseLock) {
            mouseX = StdDraw.userX(e.getX());
            mouseY = StdDraw.userY(e.getY());
        }
    }

    /* This method must not be called directly. */
    @Override
    public void mouseMoved(MouseEvent e) {
        synchronized (mouseLock) {
            mouseX = StdDraw.userX(e.getX());
            mouseY = StdDraw.userY(e.getY());
        }
    }


   /***************************************************************************
    *  Keyboard interactions.
    ***************************************************************************/

    /**
     * Returns true iff the user has typed a key (that has not yet been
     * processed).
     */
    public static boolean hasNextKeyTyped() {
        synchronized (keyLock) {
            return !keysTyped.isEmpty();
        }
    }

    /**
     * Returns the next key that was typed by the user (that your program has
     * not already processed). This method should be preceded by a call
     * to {@link #hasNextKeyTyped()} to ensure that there is a next key
     * to process.
     *
     * This method returns a Unicode character corresponding to the key
     * typed (such as {@code 'a'} or {@code 'A'}).
     * It cannot identify action keys (such as F1 and arrow keys)
     * or modifier keys (such as control).
     */
    public static char nextKeyTyped() {
        synchronized (keyLock) {
            if (keysTyped.isEmpty()) {
                throw new NoSuchElementException("your program has already "
                                                 + "processed all keystrokes");
            }
            return keysTyped.removeLast();
        }
    }

    /**
     * Returns true iff KEYCODE is being pressed.
     *
     * It can handle action keys (such as F1 and arrow keys) and modifier
     * keys (such as shift and control).
     * See {@link KeyEvent} for a description of key codes.
     */
    public static boolean isKeyPressed(int keycode) {
        synchronized (keyLock) {
            return keysDown.contains(keycode);
        }
    }


    /* This method must not be called directly. */
    @Override
    public void keyTyped(KeyEvent e) {
        synchronized (keyLock) {
            keysTyped.addFirst(e.getKeyChar());
        }
    }

    /* This method must not be called directly. */
    @Override
    public void keyPressed(KeyEvent e) {
        synchronized (keyLock) {
            keysDown.add(e.getKeyCode());
        }
    }

    /* This method must not be called directly. */
    @Override
    public void keyReleased(KeyEvent e) {
        synchronized (keyLock) {
            keysDown.remove(e.getKeyCode());
        }
    }

    /** Return an error exception with MESSAGE as its message. */
    private static IllegalArgumentException err(String message) {
        return new IllegalArgumentException(message);
    }

    /** Return an error exception with no message. */
    private static IllegalArgumentException err() {
        return new IllegalArgumentException();
    }

    /**
     * Test client.
     */
    public static void main(String[] unused) {
        StdDraw.square(.2, .8, .1);
        StdDraw.filledSquare(.8, .8, .2);
        StdDraw.circle(.8, .2, .2);

        StdDraw.setPenColor(StdDraw.BOOK_RED);
        StdDraw.setPenRadius(.02);
        StdDraw.arc(.8, .2, .1, 200, 45);

        StdDraw.setPenRadius();
        StdDraw.setPenColor(StdDraw.BOOK_BLUE);
        double[] x = { .1, .2, .3, .2 };
        double[] y = { .2, .3, .2, .1 };
        StdDraw.filledPolygon(x, y);

        StdDraw.setPenColor(StdDraw.BLACK);
        StdDraw.text(0.2, 0.5, "black text");
        StdDraw.setPenColor(StdDraw.WHITE);
        StdDraw.text(0.8, 0.8, "white text");
    }

}
